/**
 * devToolz
 * @version 0.1
 * @author klinifini
 * @copyright http://code.google.com/p/devtoolz/
 */
var devToolz = _ = new (function()
{
	var maxAlerts = 5,
		alertTimeOut = 1000,
		lastAlertTime = 0,
		alertCount = 0,
		alertFreeze = false,
		alertFunc = alert,
		self = this;
	
	/**
	 * Получение функции, которая обрабатывает ключи, в зависимости от типа obj
	 * При переборе массива через for..in тип i - строка
	 * поэтому, если перебираем массив, то преобразуем в число (индекс в массиве)
	 * иначе, возвращаем ключ как есть
	 * @param {Object} obj - объект или массив
	 * @return {function}
	 */
	var getNormalizeKeysFunc = function(obj) 
	{
		return self.isArray(obj) ? 
						        function(value) {return Number(value)}
								: function(value) {return value};
	};
	
	/**
	 * Подмена alert
	 */
	this.replaceAlert = function ()
	{
		window.alert = self.alert;
		return this;
	};

	/**
	 * Повтор строки заданное число раз
	 * @param {string} str Строка
	 * @param {integer} n Число повторений
	 * @return {string}
	 */
	this.repeat = function (str, n)
	{
		var s = '';
		while (n > 0)
		{
			s += str;
			n--
		}
		return s
	};
	
	/**
	 * Сравнение двух объектов. 
	 * @param {Object} a Объект 1
	 * @param {Object} b Объект 2
	 */
	this.isEqual = function (a, b)
	{
		if (a === b) return true;
						
		if (self.isNumber(a) && self.isNumber(b)) 
		{
			return a.toString() == b.toString();
		};
		
		if (self.isString(a) && self.isString(b)) 
		{
			return a == b;
		};
		
		if (self.isBoolean(a) && self.isBoolean(b)) 
		{
			return a.valueOf() == b.valueOf();
		};
		
		if (self.isFunction(a) && self.isFunction(b)) 
		{
			return a.toString() == b.toString();
		};
		
		if (typeof(a) != typeof(b)) return false;
		
		if (a == b) return true;
		
		if ((!a && b) || (a && !b)) return false;

		if (typeof(a.length) !== 'undefined' && (a.length !== b.length)) return false;
		
		if (self.keys(a).length != self.keys(b).length) return false;
		
		for (var key in a) 
		{
			if (!(key in b) || !self.isEqual(a[key], b[key])) 
			{
				return false;
			}
		}
	
		return true;
	};
	
	/**
	 * Проверяет наличие элемента в объекте
	 * @param {Object, array} obj Объект или список контейнер, в котором проверяется наличие
	 * @param {Object} needle Искомый объект
	 * @return {boolean}
	 */
	this.exists = function (obj, needle)
	{
		for (var i in obj)
		{
			if (self.isEqual(obj[i], needle)) return true
		};
		return false
	};
	
	/**
	 * Получение индекса или ключа элемента
	 * @param {Object, array} obj Контейнер
	 * @param {Object, function} needle Искомый объект, или функция function(value, key, obj)
	 * @param {object} [context] - Контекст функции
	 * @return {integer, string, null} Индекс или ключ найденного элемента или null в случае отсутствия
	 */
	this.indexOf = this.keyOf = function (obj, needle, context)
	{
		// Преобразование ключа в число, если obj - массив
        var decorateValueFunc = getNormalizeKeysFunc(obj);
		
		// функция теста значения
		// если needle - функция, то используем её, иначе - функцию сравнения
		var findVal = needle;
		var testFunc = self.isFunction(needle) ? 
							needle
							: function(value, key, obj)	{return self.isEqual(value, findVal)};
						
		for (var i in obj)
		{
			if (testFunc.call(context, obj[i], i, obj))
			{
				return decorateValueFunc(i);
			}
		};
		return null;
	};
		
	/**
	 * Получение всех индексов или ключей элемента
	 * @param {Object, array} obj Контейнер
	 * @param {Object} needle Искомый объект
	 * @return {array} Массив индексов или ключей
	 */
	this.indexesOf = this.keysOf = function (obj, needle, context)
	{
		var result = [];
		
		// Преобразование ключа в число, если obj - массив
        var normalizeKeyFunc = getNormalizeKeysFunc(obj);
		
		
		// функция теста значения
		// если needle - функция, то используем её, иначе - функцию сравнения
		var findVal = needle;
		var testFunc = self.isFunction(needle) ? 
							needle
							: function(value, key, obj)	{return self.isEqual(value, findVal)};
							
		for (var i in obj)
		{
			if (testFunc.call(context, obj[i], i, obj))
			{
				result.push( normalizeKeyFunc(i) );
			}
		};
		return result
	};
	
	/**
	 * Вычитание одного объекта из другого
	 * @param {Object,array,string,number} obj Уменьшаемый объект
	 * @param {Object,array,string,number} subtr Вычитаемый объект
	 * @return {object,array}
	 */
	this.subtract = function (obj, subtr)
	{
		var result;
		if (typeof(obj) == 'string' && typeof(subtr) == 'string') return obj.replace(new RegExp(subtr, 'g'), '');
		if (typeof(obj) == 'number' && typeof(subtr) == 'number') return obj - subtr;
		if (self.isArray(obj) && self.isArray(subtr))
		{
			result = [];
			for (var i = 0; i < obj.length; i++)
			{
				if (!self.exists(subtr, obj[i]))
				{
					result.push(obj[i])
				}
			};
			return result
		}
		result = self.clone(obj);
		for (var i in obj)
		{
			if (typeof(subtr[i]) != 'undefined' && self.isEqual(obj[i], subtr[i])) delete(result[i])
		};
		return result
	};
	
	/**
	 * Фильтровать список
	 * @param {array,object} obj - фильтруемый объект
	 * @param {object,function} element - искомые элементы, или функция function(item, key, obj)
	 * @param {bool, Object} [funcResult=true] - результат функции, при котором элемент включается в список
	 */
	this.filter = function (obj, element, funcResult) 
	{
		var result, func, addItemFunc;
		funcResult = typeof(funcResult) == 'undefined' ? true : funcResult;
		
		if (typeof(obj) == 'string' && typeof(element) == 'string') return obj.replace(new RegExp(element, 'g'), '');
		if (typeof(obj) == 'number' && typeof(element) == 'number') return obj - element;
		
		
		if (self.isFunction(element))
		{
			func = element;		// передана функция отсеивания
		}
		else
		{
			var el = element;
			func = function(item) {return self.isEqual(el, item); }
		};
		
		
		
		result = self.newLike(obj);
		
		/*
		 * Различные варианты добавления элемента для объектов и для массивов
		 */
		if (self.isArray(obj))
		{
			addItemFunc = function(item) { result.push(item) };
		}
		else
		{
			addItemFunc = function(item, key) { result[key] = item };
		};
		
		self.each(obj, function(item, key) 
			{
				if (func(item, key, obj) == funcResult)		// добавляем
				{
					addItemFunc(item, key);
				}
			});
		
		return result;
	};
	
	/**
	 * Удаление элемента из объекта
	 * @param {Object,array,string,number} obj Уменьшаемый объект
	 * @param {Object} element Элемент или функция, если true - элемент удаляется
	 * @return {object,array}
	 */
	this.remove = function (obj, element)
	{
		return self.filter(obj, element, false);	// оставляем те, где результат отрицателен
	};
	
	/**
	 * Удаление элементов из массива или объекта по индексу или ключу
	 * @param {array,object} obj - массив или объект
	 * @param {array,string,number} keys - индекс или ключ или массив индексов или ключей удаляемыъ элементов
	 */
	this.removeAt = this.removeWith = function(obj, keys) 
	{
		if (self.isString(keys) || self.isNumber(keys)) 
		{
			keys = [keys];
		};
		
		// сортируем индексы в обратном порядке, чтобы
		// при удалении удалялись ключи с хвоста, тем самым не сдвигая элементы
		if (self.isArray(obj)) 
		{
			keys.sort(function(left, right)
			{
				return left == right ? 0 : left < right ? 1 : -1;
			});
		};
		
		// функция удаления элемента
        var deleteFunc = self.isArray(obj) ? function(obj, index)
								        {
								            obj.splice(index, 1);
								        }
								     : function(obj, key)
								        {
								            delete obj[key];
								        };
		
		var result = self.clone(obj);
		
		self.each(keys, function(key)
		{
			deleteFunc(result, key);
		});
		
		return result;
	};
	
	/**
	 * Приводит объект к строке в читабельном виде
	 * @param {Object} obj Объект
	 * @param {int} [maxdepth=+inf] Максимальный уровень, до которого происходит дамп, после вызывается toString()
	 * @param {int} [level=0] Уровень вложенности (кол-во отступов новой строки)
	 * @return {string}
	 */
	this.dump = function (obj, maxdepth, level)
	{
		maxdepth = typeof(maxdepth) == 'undefined' ? Number.POSITIVE_INFINITY : maxdepth;
		var level = level || 0;

		if (level >= maxdepth) 
		{
			return obj.toString();
		};
		
		
		var result = '';
		var space = self.repeat("    ", level);
		var node;
		
		
		
		if (typeof(obj) == 'string' || typeof(obj) == 'number' || typeof(obj) == 'boolean') return obj;

		function formatNode(node)
		{
			switch (typeof(node))
			{
			case 'string':
				node = "\u201C" + node + "\u201D";
				break;
			case 'function':
				if (typeof(node.toSource) == 'undefined')
				{
					node = node.toString()
				}
				else
				{
					node = node.toSource()
				};
				break;
			case 'object':
			
				node = self.dump(node, maxdepth, level + 1);
				
				break;
			default:
				node = node
			}
			return node
		}
		
		
		
		if (typeof(obj) == 'object' && obj instanceof Array)
		{
			for (var i = 0; i < obj.length; i++) result += "\n" + space + i + ': ' + formatNode(obj[i])
		}
		else
		{
			for (var i in obj)
			{
				if (!obj.hasOwnProperty(i))
				{
					continue
				};
				result += "\n" + space + '[' + i + ']: ' + formatNode(obj[i])
			}
		};
		
		if (level == 0)
		{
			result = result.substr(1)
		};
		return result
	};
	
	/**
	 * Клонирование объекта
	 * @param {Object} obj
	 * @return {object}
	 */
	this.clone = function (obj)
	{
		if (typeof(obj) == 'number' || typeof(obj) == 'string' || typeof(obj) == 'function' || typeof(obj) == 'boolean') return obj;
		
		if (self.isArray(obj)) return obj.slice();
		
		var result = self.newLike(obj);
		
		for (var i in obj)
		{
			if (!obj.hasOwnProperty(i)) continue;
			
			result[i] = obj[i];
		};
		
		return result
	};
	
	/**
	 * Создание нового объекта, по образу другого
	 * @param {Object} obj - объект-прототип
	 */
	this.newLike = function(obj)
	{
		if (self.isArray(obj))
		{
			return [];
		}
		else
		{
			/*
			 * Создаём новый пустой конструктор класса, проторипом которого
			 * будет прототип конструктора исходного класса
			 */
			var constr = function(){};
			constr.prototype = obj.constructor.prototype;
			
			/*
			 * Создаём новый экземпляр через наш конструктор
			 * тем самым прототипом объекта будет прототип исходного класса
			 */
			result = new constr();
			
			return result;
		}
	};
	
	/**
	 * Объект, возвращение итератором которого означает, что необходимо прервать цикл
	 */
	this.breaker = {};
	
	/**
	 * Вызов функции для каждого элемента коллекции
	 * @param {array, object} obj - коллекция
	 * @param {function} iterator - функция function(элемент, ключ, коллекция). Если функция возвращает devToolz.breaker - итерация прерывается
	 * @param {object} [context] - контекст функции
	 * @param {object} [arg1] - аргумент будет передан функции
	 */
	this.each = function(obj, iterator, context) 
	{
		/*
		 * Собираем новый массив, из остальных параметров, кроме первых трёх
		 * чтобы потом передавать их iterator
		 */
		var args = Array.prototype.slice.call(arguments,3);
        args.unshift(null, null, obj); // 0 - элемент, 1 - ключ, 2 - коллекция
		
		// Преобразование ключа в число, если obj - массив
        var normalizeKeyFunc = getNormalizeKeysFunc(obj);
		
		for (var i in obj) 
		{
			if (!obj.hasOwnProperty(i))
			{
				continue
			};
			
			args[0] = obj[i];
			args[1] = normalizeKeyFunc(i);
			var result = iterator.apply(context, args);
			// Если итератор возвращает false - прекращаем итерирование
			if (result === self.breaker)
			{
				break; 
			}
				
		};
	};
	
	/**
	 * Замена всех элементов объекта результатом функции, создаётся копия обзекта
	 * Перечисляются все поля объекта, в том числе и функции
	 * @param {object} obj - исходный объект
	 * @param {function} iterator - функция
	 * @param {object} [context] - контекст функции
	 * @param {object} [arg1] - последующие параметры передаются функции-итератору
	 * @return {object,array}
	 */
	this.map = function(obj, iterator, context) 
	{
		var args = Array.prototype.slice.call(arguments,3);
		args.unshift(null, null , obj); // 0 - элемент, 1 - ключ, 2 - коллекция
		
		// Преобразование ключа в число, если obj - массив
        var normalizeKeyFunc = getNormalizeKeysFunc(obj);
		
		/*
		 * Создаём новый объект, подобный текущему
		 */
		var result = self.newLike(obj);
				
		self.each(
				obj,
				
				function(item, key, obj) 
				{
					args[0] = item;
					args[1] = normalizeKeyFunc(key);
					result[key] = iterator.apply(context,args);			
				},
				
				context);
	
		return result;
	};
	
	/**
	 * Получить из массива объектов значения полей, с ключём key
	 * @param {array,object} arr - коллекция
	 * @param {Object} key - ключ
	 * @return {array}
	 */
	this.withKey = function(arr, key) 
	{
		var result = [];
		self.each(arr, function(item)
			{
				result.push(item[key]);
			});
		return result;
	};
	
	/**
	 * Извлечь массив значений из объекта
	 * @param {Object} obj
	 * @return {array}
	 */
	this.values = function(obj) 
	{
		var result = [];
		self.each(obj, function(item)
			{
				result.push(item);
			});
		return result;
	};
	
	/**
	 * Извлечь массив ключей из объекта
	 * @param {Object} obj
	 * @return {array}
	 */
	this.keys =  function(obj) 
	{
		var result = [];
		
		// Преобразование ключа в число, если obj - массив
        var normalizeKeyFunc = getNormalizeKeysFunc(obj);
		
		self.each(obj, function(item, key)
			{
				result.push( normalizeKeyFunc(key) );
			});
		return result;
	};
	
	/**
	 * Сортировать массив объектов по критерию (создаётся копия объекта)
	 * @param {array} arr - массив объектов
	 * @param {function,string,array} criteria - функция function(item, key, list), задающая критерий, или имя свойства объектов, или массив свойств для сортировки по нескольким полям. Если в имени поля первым символом является !, то сортировка по этому полю будет в обратном порядке.
	 * @param {Object} context
	 * @return {array,object}
	 */
	this.sortBy = function(arr, criteria, context) 
	{
		var result = self.clone(arr);
		// символ, указывающий, что сортировку по полю надо производить в обратном порядке
		var reverseFlag = '!';
		
		if (self.isString(criteria))
		{
		/*
		 * Задано имя свойства
		 */
			criteria = [criteria];	// оборачиваем в массив
		};
		
		if (self.isArray(criteria))
		{
		/*
		 * Задано массив свойств
		 */
			var props = criteria;
			
			result.sort(function(left, right)
			{
				for (var i=0; i<props.length; i++)	// перебираем все свойства по порядку, пока не найдём разницу
				{
					var propName = props[i];
					
					var direction = 1;		// прямое направление сортировки
					if (propName.charAt(0) == reverseFlag) 
					{
						propName = propName.substr(1);		// удаляем флаг
						direction = -1;		// обратное направление
					};
			
					if (left[propName] > right[propName]) return 1 * direction;
					if (left[propName] < right[propName]) return -1 * direction;
				};
				return 0;		// разницы нет ни в одном из свойств
			});
			
			return result;
		}
		else 
		{
		/*
		 * Задана функция, вычисляющая критерий сортировки
		 */
			var iterator = criteria;
			/*
			 * Оборачиваем каждый элемент в объект
			 * внутри которого вычисляем критерии сортировки
			 */
			var objects = self
				.map(result,function(item, key) 
				{
					return {
						criteria: iterator.call(context, item, key, arr),
						value: item
					};
				})
				.sort(function(left,right)		// сортируем по критерию
				{
					var a = left.criteria, b = right.criteria;
		      		return a < b ? -1 : a > b ? 1 : 0;
				});
				
			return _.withKey(objects, 'value');		// извлекаем объекты
		}
	};

	this.date = 
	{
		/**
		 * Текущее время в милисекундах
		 * @return {Integer}
		 */
		now: function ()
		{
			return (new Date().getTime())
		},
		
		/**
		 * Количество прошедших милисекунд
		 * @param {integer} old Старое время
		 * @param {integer} now Новое время [текущее]
		 */
		elapsedTime: function (old, now)
		{
			now = now || self.date.now();
			return (now - old)
		}
	};
	
	this.isArray = function (obj)
	{
		return (typeof(obj) == 'object' && obj instanceof Array)
	};
	this.isString = function (obj)
	{
		return (typeof(obj) == 'string' || (typeof(obj) == 'object' && obj instanceof String))
	};
	this.isNumber = function (obj)
	{
		return (typeof(obj) == 'number' || (typeof(obj) == 'object' && obj instanceof Number))
	};
	this.isBoolean = function (obj)
	{
		return (typeof(obj) == 'boolean' || (typeof(obj) == 'object' && obj instanceof Boolean))
	};
	this.isFunction = function (obj)
	{
		return (typeof(obj) == 'function')
	};
	this.isRegExp = function (obj)
	{
		return (typeof(obj) == 'object' && obj instanceof RegExp)
	};
	
	/**
	 * Вывести alert, используя dump для сообщения
	 * @param {Object} message - объект, который будет выведен
	 * @param {int} [maxdepth=+inf] - максимальный уровень вложенности
	 */
	this.alert = function (message, maxdepth)
	{
		message = self.dump(message, maxdepth);

		if (self.date.elapsedTime(lastAlertTime) < alertTimeOut) alertCount++;
		else
		{
			alertCount = 0;
			alertFreeze = false
		};
		if (alertFreeze)
		{
			lastAlertTime = self.date.now();
			return
		};
		if (alertCount >= maxAlerts)
		{
			if (!confirm('Продолжить показ всплывающих окон?'))
			{
				alertFreeze = true
			}
			else alertCount = 0
		};
		if (alertCount < maxAlerts)
		{
			alertFunc.call(window, message)
		};
		lastAlertTime = self.date.now()
	};
})();
